#pragma once

#include <boost/iostreams/device/mapped_file.hpp>
#include <boost/filesystem/operations.hpp>

#include "../../exception.hpp"
#include "../../util/verify.hpp"




namespace boostext
{
	namespace iostreams
	{
		using namespace boost::iostreams;


		class mapped_file_source : public device<input>
		{
		public:

			/// This mapped_file_source only operates on characters
			typedef char      char_type;

		private:

			/// This is the maximum size (in bytes) that is mapped into the process'es address space
			/// at once. Bigger files will be mapped in chunks (when the appropriate regions must be
			/// be read).
			static const std::streamsize m_mappingSize = 10 * 1024 * 1024;



			/// That's the total size of the file (determined at startup and never touched again)
			std::streamsize   m_size;

			/// The parameters, used to map the file into our address space
			mapped_file_params   m_params;

			/// That's the actual implementation of the memory mapped file
			boost::iostreams::mapped_file_source   m_source;

			/// That's the current position, relative to the start of the entire file
			std::streamsize   m_pos;

		public:

			/**
			 * Create a mapped_file_source device.
			 */
			mapped_file_source(const std::string& path)
				: m_size(boost::filesystem::file_size(path))
				, m_pos(0)
			{
				m_params.path   = path;
				m_params.length = std::min(m_size, m_mappingSize);
				m_params.offset = 0;

				remap(m_pos);
			}



			/**
			 * Reads size bytes from the device into data.
			 *
			 * @param data   pointer to the buffer to write the content of the istream to
			 * @param n      number of bytes to read
			 *
			 * @throws   boost_ext::ios_failure when the data cannot be read
			 */
			std::streamsize read(void* data, std::streamsize n)
			{
				// Reading 0 bytes is legal
				if(n == 0)
					return 0;

				// Defines the boundaries of what we want to read
				std::streamsize startRead = m_pos;
				std::streamsize endRead   = m_pos + n;

				std::streamsize startMapped = m_params.offset;
				std::streamsize endMapped   = m_params.offset + m_params.length;

				// A special requirement is, that it's not allowed
				// to read more, than half of what is mapped: this is the maximum
				// size that is garanteed to be mapped into our address space
				verify_range(n, 0, m_mappingSize / 2);
				verify_range(startRead, 0, m_size);
				verify_range(endRead, 0, m_size);
				verify_pointer(data);

				if(endRead < startMapped || startRead > endMapped)
				{
					// What we are trying to read is entirely out of the currently mapped space:
					// Remap the file to gain access to the desired portion of it.
					remap(m_pos);
					::memcpy(data, &m_source.data()[m_pos - m_params.offset], n);
					seek(n, std::ios_base::cur);
				}
				else if(startRead >= startMapped && endRead <= endMapped)
				{
					// What we are trying to read is entirely inside the mapped space
					::memcpy(data, &m_source.data()[m_pos - m_params.offset], n);
					seek(n, std::ios_base::cur);
				}
				else if(startRead < endMapped && endRead > endMapped )
				{
					// We are reading something, that's on the upper end of the mapped file, while some
					// of the requested data is above what is mapped
					std::streamsize half1 = endRead - endMapped;
					std::streamsize half2 = n - half1;

					read(data, half1);
					read(&((char*)data)[half1], half2);
				}
				else if(startRead < startMapped && endRead > startMapped)
				{
					// We are reading something that's on the lower end of the mapped file, while some
					// of the requested data is beneath what is mapped
					// We thus read the 2nd half first
					std::streamsize half2 = endRead - startMapped;
					std::streamsize half1 = n - half2;

					read(&((char*)data)[half1], half2);
					read(data, half1);
				}

				return n;
			}

			/**
			 * Gain direct read-only access to a portion of the stream.
			 * The returned pointer, points to the first byte, at the stream's
			 * current position and it's length is no more than n.
			 *
			 * @warning the block of memory may only be used, until other function of the stream is called, then the pointer will be invalidated
			 *
			 * @info Calling this function does not modify the stream's position.
			 *
			 * @throws   boost_ext::ios_failure when the range cannot be satisfied
			 */
			const void* directSequence(std::streamsize n)
			{
				// Defines the boundaries of what we want to read
				std::streamsize startRead = m_pos;
				std::streamsize endRead   = m_pos + n;

				std::streamsize startMapped = m_params.offset;
				std::streamsize endMapped   = m_params.offset + m_params.length;

				// A special requirement is, that it's not allowed
				// to read more, than half of what is mapped: this is the maximum
				// size that is garanteed to be mapped into our address space
				verify_range(n, 0, m_mappingSize / 2);
				verify_range(startRead, 0, m_size);
				verify_range(endRead, 0, m_size);

				if(startRead < startMapped || endRead > endMapped)
				{
					// What we are trying to read is not entirely inside the mapped space
					// remap it to fit
					remap(m_pos);
					
				}

				return &m_source.data()[m_pos - m_params.offset];
			}

			std::streamsize size() const
			{
				return m_size;
			}

			std::streamsize seek(stream_offset off, std::ios_base::seekdir way)
			{
				switch(way)
				{
				case std::ios_base::beg:
					{
						verify_range(off, 0, m_size);
						m_pos = off;
					}
					break;
				case std::ios_base::cur:
					{
						boost::iostreams::stream_offset pos = m_pos + off;
						verify_range(pos, 0, m_size);
						m_pos += off;
					}
					break;
				case std::ios_base::end:
					{
						boost::iostreams::stream_offset pos = m_pos + off;
						verify_range(pos, 0, m_size);
						m_pos += off;
					}
					break;
				default: BOOSTEXT_THROW(invalid_value_exception(way));
				}

				// When seeking, the file needs to be remapped, when the new position
				// points outside of what is currently mapped.
				if(m_pos < m_params.offset || m_pos > m_params.offset + m_params.length)
				{
					remap(m_pos);
				}

				return m_pos;
			}

		private:

			/**
			 * Maps a portion of the file into the process'es address space.
			 * The function's MO can be explained with the following table:
			 * - pos: 0iB,    mapping [0iB, 1GiB]
			 * - pos: 0.5GiB, mapping [0iB, 1GiB]
			 * - pos: 1GiB,   mapping [0.5GiB, 1.5GiB]
			 */
			void remap(std::streamsize pos)
			{
				std::streamsize mappingStart = 0;
				std::streamsize mappingEnd   = 0;

				if(pos < m_mappingSize / 2)
				{
					// Map everything from the first byte onwards;
					mappingStart = 0;
					mappingEnd   = std::min(m_size, m_mappingSize);
				}
				else if(pos + m_mappingSize / 2 > m_size)
				{
					// Map everything from the last byte backwards
					mappingEnd   = m_size;
					mappingStart = mappingEnd - m_mappingSize;
				}
				else
				{
					// Map exactly mappingSize bytes
					mappingStart = pos - m_mappingSize / 2;
					mappingEnd   = pos + m_mappingSize / 2;
				}


				// Now test if the offset (start) fits into the system's alignment.
				// If it does not, then it is shifted towards the end (it's more likely
				// that a stream is read forwards).
				std::streamsize alignment = mapped_file::alignment();
				std::streamsize mod = mappingStart % alignment;
				if(mod != 0)
				{
					mappingStart += alignment - mod;
				}


				// Remap the new portion of the file
				// When it doesn't work we throw an exception
				try
				{
					m_params.length = mappingEnd - mappingStart;
					m_params.offset = mappingStart;

					if(m_source.is_open())
						m_source.close();
					m_source.open(m_params);
				}
				catch(const std::ios_base::failure& e)
				{
					BOOSTEXT_THROW(ios_failure(e.what()));
				}
			}
		};
		///////////////////////////////////////////////////////////////////////////////////////////
	}
	///////////////////////////////////////////////////////////////////////////////////////////////
}
///////////////////////////////////////////////////////////////////////////////////////////////////
